/** @file f_lzostream.cpp
	@brief LZO compress / decompress filter for streams */

#include "file/f_lzostream.h"

using namespace G3;

//==============================================================================
// Constructors & Destructors
//==============================================================================
G3::LZOSTREAM::LZOSTREAM (STREAM *stream):
        ZSTREAM (stream) {

}

G3::LZOSTREAM::~LZOSTREAM () {
    if (IsOpen (0))
        Close ();
}

//==============================================================================
// Opens a ZFILESTREAM either for Deflating or Inflating data..
//
//		byte OpenFlags				Flags for the stream
//				ZSTREAM_DEFLATE		Opens for deflating (compressed buffering)
//				ZSTREAM_INFLATE		Opens for inflating (decompressed reading)
//
//		int Level					Compression level
//				-1					Default, level 6
//				0					No compression at all
//				1 ... 9				Lowest ... Hightest compression
//
//	Returns true if successful
//==============================================================================
bool G3::LZOSTREAM::Open (byte openFlags, int level) {
    Assert (bStream, "LZOSTREAM::Open: bStream member is a NULL pointer..\n");
    Assert (level >= -1 && level <= 9 || level == 99 || level == 999, "LZOSTREAM::Open: Invalid compression level");

    // Already open?
    if ((flags & ZSTREAM_OPEN) != 0) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::Open: Stream already open.\n");
        return false;
    }

    if (level <= 9)
        compressionLevel = (char) level;
    else if (level == 99)
        compressionLevel = 10;
    else if (level == 999)
        compressionLevel = 11;

    // Should never fail
    Assert (lzo_init () == LZO_E_OK, "LZOSTREAM::Open: lzo_init failed. Try recompiling without optimizations and enable `-DLZO_DEBUG' for diagnostics\n");

    // Are we asked to deflate or inflate?
    if ((openFlags & ZSTREAM_DEFLATE) != 0) {
        flags |= ZSTREAM_DEFLATE;
    } else {
        flags |= ZSTREAM_INFLATE;
    }

    // Mark our stream open
    flags |= ZSTREAM_OPEN;

    return true;
}

//==============================================================================
// Closes the LZOSTREAM
//==============================================================================
void G3::LZOSTREAM::Close () {
    flags = 0;
}

//==============================================================================
// Compresses data and adds it to the stream buffer
//
//		void *Pointer			Pointer to a memory block from where to compress
//
//		long Size				Number of bytes to compress
//
//	Returns true on success
//==============================================================================
bool G3::LZOSTREAM::Deflate (byte *pointer, long size) {
    Assert (bStream, "LZOSTREAM::Deflate: bStream member is a NULL pointer..\n");

    if (!pointer)
        return true;


    if (!IsOpen (ZSTREAM_DEFLATE)) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::Deflate: Stream is not opened for deflating (Flags: 0x%X)..\n", flags);
        return false;
    }

    byte *data2 = (byte *) malloc (LZO_PACKED_SIZE (size));
    long outSize = 0;

    if (data2 == NULL) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::Deflate: Not enough memory for deflating (at least %ld more bytes needed)..\n", outputChunkSize + 1);
        return false;
    }

    outSize = DoCompress (pointer, size, data2);

    if (outSize < 0)
        return false;

    if (outSize >= size) {
        if (log)
            log->Report ("LZOSTREAM::Deflate: Contains incompressible block..\n");
    }

    // Buffer the freshly compressed data
    bStream->Buffer (data2, outSize);

    free (data2);
    data2 = NULL;

    return true;
}

//==============================================================================
// Reads data from the stream and decompresses it
//
//		void *Pointer			Pointer to a memory block where the data is stored
//
//		long Size				Number of bytes saved at the Pointer
//
//	Returns the number of bytes stored at Pointer
//==============================================================================
long G3::LZOSTREAM::Inflate (byte *pointer, long size) {
    Assert (bStream, "LZOSTREAM::Inflate: bStream member is a NULL pointer..\n");
    Assert (pointer, "LZOSTREAM::Inflate: Pointer argument is a NULL pointer");
    Assert (size > 0, "LZOSTREAM::Inflate: Size argument is too small (<= 0)");

    if (!IsOpen (ZSTREAM_INFLATE)) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::Inflate: Stream is not opened for inflating (Flags: %d)..\n", flags);
        return -1;
    }

    int ret = 0;
    long sizeIn = LZO_UNPACKED_SIZE (size);
    long resultSize = 0;
    uint tempSize = 0;

    // Is someone flushing / buffering already?
    if (bStream->bufferReadLocked)
        return -1;

    // Make sure nobody is moving our caret during the read
    bStream->caretLocked = true;
    // Make sure the buffer data pointer stays constant during our read
    bStream->bufferWriteLocked = true;

    STREAM::CHUNK chunk;

    while (true) {
        chunk = bStream->GetChunk ();

        if (sizeIn <= chunk.chunkSize) {
            ret = lzo1x_decompress (chunk.chunkPointer, sizeIn, pointer, &tempSize, NULL);
            resultSize += tempSize;

            bStream->Seek (chunk.chunkSize, STREAM::STREAM_MOVE);
            break;
        } else {
            ret = lzo1x_decompress (chunk.chunkPointer, chunk.chunkSize, pointer, &tempSize, NULL);
            resultSize += tempSize;

            bStream->Seek (chunk.chunkSize, STREAM::STREAM_MOVE);
        }

        if (ret != LZO_E_OK) {
            if (log)
                log->Report ("ERROR! LZOSTREAM::Inflate: Decompression failed..\n");

            // Free the caret to other threads
            bStream->caretLocked = false;
            // Free the buffer to other threads
            bStream->bufferWriteLocked = false;

            return -1;
        }
    }

    // Free the caret to other threads
    bStream->caretLocked = false;
    // Free the buffer to other threads
    bStream->bufferWriteLocked = false;

    // Number of bytes stored at Pointer
    return resultSize;
}

//==============================================================================
// Decompresses data into the stream
//
//		void *Pointer			Pointer to a memory block where the data is read from
//
//		long Size				Number of bytes read from the Pointer
//
//	Returns the number of bytes decompressed into the stream
//==============================================================================
long G3::LZOSTREAM::InflateIn (byte *pointer, long size) {
    Assert (bStream, "LZOSTREAM::InflateIn: bStream member is a NULL pointer..\n");
    Assert (pointer, "LZOSTREAM::InflateIn: Pointer argument is a NULL pointer");

    if (!IsOpen (ZSTREAM_INFLATE)) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::InflateIn: Stream is not opened for deflating (Flags: %d)..\n", flags);
        return false;
    }

    byte *data2 = new byte [LZO_UNPACKED_SIZE (size + 1)];
    uint outSize = LZO_UNPACKED_SIZE (size + 1);
    long ret = 0;

    if (data2 == NULL) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::InflateIn: Not enough memory for deflating (at least %ld more bytes needed)..\n", LZO_UNPACKED_SIZE (size));
        return false;
    }

    // For some reason, LZO decompress needs the destination buffer to be filled with zeroes
    memset (data2, 0, outSize - 1);

    ret = lzo1x_decompress_safe (pointer, size, data2, &outSize, NULL);

    if (ret != LZO_E_OK) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::InflateIn: Decompression failed (error %d)..\n", ret);

        // Free the caret to other threads
        bStream->caretLocked = false;
        // Free the buffer to other threads
        bStream->bufferWriteLocked = false;

        return -1;
    }

    bStream->Buffer (data2, outSize);

    delete [] data2;
    data2 = NULL;

    return outSize;
}

//==============================================================================
// Compresses data from the stream
//
//		void *Pointer			Pointer to a memory block where to compress
//
//		long Size				Number of bytes to compress
//
//	Returns true on success
//==============================================================================
long G3::LZOSTREAM::DeflateOut (byte *pointer, long size) {
    Assert (bStream, "LZOSTREAM::DeflateOut: bStream member is a NULL pointer..\n");
    Assert (pointer, "LZOSTREAM::DeflateOut: Pointer argument is a NULL pointer");
    Assert (size > 0, "LZOSTREAM::DeflateOut: Size argument is too small (<= 0)");

    if (!IsOpen (ZSTREAM_DEFLATE)) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::DeflateOut: Stream is not opened for deflating (Flags: %d)..\n", flags);
        return -1;
    }

    // Is someone flushing / buffering already?
    if (bStream->bufferReadLocked)
        return 0;

    // Make sure nobody is moving our caret during the read
    bStream->caretLocked = true;
    // Make sure the buffer data pointer stays constant during our read
    bStream->bufferWriteLocked = true;

    STREAM::CHUNK chunk;
    long outSize = 0;
    long written = 0;

    while (true) {
        chunk = bStream->GetChunk ();

        if (size <= chunk.chunkSize) {
            outSize = DoCompress (chunk.chunkPointer, size, pointer);

            if (outSize < 0) {
                // Free the caret to other threads
                bStream->caretLocked = false;
                // Free the buffer to other threads
                bStream->bufferWriteLocked = false;

                return -1;
            }

            written += outSize;

            bStream->Seek (size, STREAM::STREAM_MOVE);
            break;
        } else {
            outSize = DoCompress (chunk.chunkPointer, chunk.chunkSize, pointer);

            if (outSize < 0) {
                // Free the caret to other threads
                bStream->caretLocked = false;
                // Free the buffer to other threads
                bStream->bufferWriteLocked = false;

                return -1;
            }

            written += outSize;

            bStream->Seek (chunk.chunkSize, STREAM::STREAM_MOVE);
        }
    }

    // Free the caret to other threads
    bStream->caretLocked = false;
    // Free the buffer to other threads
    bStream->bufferWriteLocked = false;

    // Number of bytes stored at Pointer
    return written;
}

//==============================================================================
// Reads data from a file and decompresses it
//
//		const char *Filename	Name of the file to buffer
//
//		long Size				Number of bytes to buffer
//				-1, 0			Buffers the whole file into the stream
//
//		long Position			Where in the file should we start reading
//
//	Returns true on success
//==============================================================================
long G3::LZOSTREAM::InflateFromFile (const string &filename, long size, long position) {
    long fsize = 0;
    long chunkSize = size;
    long ret = 0;
    byte *data2 = NULL;

    Assert (bStream, "LZOSTREAM::InflateFromFile: bStream member is a NULL pointer..\n");
    Assert (!filename.empty (), "LZOSTREAM::InflateFromFile: Filename argument is a NULL pointer");

    fsize = GetFileSize (filename);

    Assert (position >= 0 && position < fsize, "LZOSTREAM::InflateFromFile: Position argument is invalid (< 0 || >= Filesize)");

    // Make sure the ChunkSize is valid
    if (size <= 0)
        chunkSize = fsize - position;
    if (size > fsize)
        chunkSize = fsize;

    // Try to open the file for binary reading
    FILE *fi = fopen (filename.c_str (), "rb");
    if (!fi) {
        if (log)
            log->Report ("ERROR! ZIPSTREAM::InflateFromFile: Failed to open the file \"%s\"..\n", filename.c_str ());
        return 0;
    }

    // Set the filename member
    bStream->fileName = filename;
    // Set the start of the file
    //SetFileStart ();

    // Make a temporary buffer
    data2 = new byte [chunkSize + 1];

    // Seek to the asked Position
    fseek (fi, position, SEEK_SET);

    // Read the file
    ret = fread (data2, 1, chunkSize, fi);

    // Buffer the stream from our temporary buffer
    InflateIn (data2, ret);

    // Free our temporary buffer
    delete [] data2;

    fclose (fi);

    // Set the size of the buffered file
    //SetFileSize (ret);

    return ret;
}

//==============================================================================
// Reads data from the stream and compresses it into a file
//
//		const char *Filename	Name of the file to store the data in
//
//		long Size				Number of bytes read from the stream
//				-1, 0			Reads the whole stream buffer
//
//		long Position			Where to write into the file
//
//	Returns the number of bytes read
//==============================================================================
long G3::LZOSTREAM::DeflateToFile (const string &filename, long size, long position) {
    long fsize = 0;
    long chunkSize = size;
    long blankSize = 0;
    long ret = 0, i = 0;
    long written = 0;
    byte blank = 0;
    byte *data2 = NULL;

    Assert (bStream, "LZOSTREAM::Open: bStream member is a NULL pointer..\n");
    Assert (!filename.empty (), "LZOSTREAM::DeflateToFile: Filename argument is a NULL pointer");

    fsize = GetFileSize (filename);

    if (size <= 0)
        chunkSize = bStream->Tell (G3::STREAM::STREAM_END);

    if (position > fsize)
        blankSize = position - fsize;

    Assert (position >= 0, "ZIPSTREAM::DeflateToFile: Position argument is invalid");

    FILE *fo = fopen (filename.c_str (), "wb");
    if (!fo) {
        if (log)
            log->Report ("ERROR! ZIPSTREAM::DeflateToFile: Failed to open the file \"%s\"..\n", filename.c_str ());
        return 0;
    }

    fseek (fo, position, SEEK_SET);

    for (i=0; i<blankSize; i++)
        fwrite (&blank, blankSize, 1, fo);

    data2 = new byte [chunkSize + 1];

    ret = DeflateOut (data2, chunkSize);

    fwrite (data2, ret, 1, fo);

    delete [] data2;

    fclose (fo);

    return ret;
}

//==============================================================================
// Calls LZO functions according to the value of the compressionLevel member
//==============================================================================
long LZOSTREAM::DoCompress (byte *pointer, long size, byte *pOut) {
    uint outSize = 0;
    byte *wrkmem = NULL;
    int ret = 0;

    switch (compressionLevel) {
    case 1:
        wrkmem = (byte *) malloc (LZO1X_1_11_MEM_COMPRESS);

        if (wrkmem == NULL) {
            if (log)
                log->Report ("ERROR! LZOSTREAM::Deflate: Not enough memory for compression (at least %ld more bytes needed)..\n", LZO1X_1_11_MEM_COMPRESS);

            free (pOut);
            return false;
        }

        ret = lzo1x_1_11_compress (pointer, size, pOut, &outSize, wrkmem);
        break;

    case 2:
        wrkmem = (byte *) malloc (LZO1X_1_12_MEM_COMPRESS);

        if (wrkmem == NULL) {
            if (log)
                log->Report ("ERROR! LZOSTREAM::Deflate: Not enough memory for compression (at least %ld more bytes needed)..\n", LZO1X_1_12_MEM_COMPRESS);

            free (pOut);
            return false;
        }

        ret = lzo1x_1_12_compress (pointer, size, pOut, &outSize, wrkmem);
        break;

    default:
    case 3:
        wrkmem = (byte *) malloc (LZO1X_1_MEM_COMPRESS);

        if (wrkmem == NULL) {
            if (log)
                log->Report ("ERROR! LZOSTREAM::Deflate: Not enough memory for compression (at least %ld more bytes needed)..\n", LZO1X_1_MEM_COMPRESS);

            free (pOut);
            return false;
        }

        ret = lzo1x_1_compress (pointer, size, pOut, &outSize, wrkmem);
        break;

    case 4:
        wrkmem = (byte *) malloc (LZO1X_1_15_MEM_COMPRESS);

        if (wrkmem == NULL) {
            if (log)
                log->Report ("ERROR! LZOSTREAM::Deflate: Not enough memory for compression (at least %ld more bytes needed)..\n", LZO1X_1_15_MEM_COMPRESS);

            free (pOut);
            return false;
        }

        ret = lzo1x_1_15_compress (pointer, size, pOut, &outSize, wrkmem);
        break;

    case 5:
        wrkmem = (byte *) malloc (LZO1X_999_MEM_COMPRESS);

        if (wrkmem == NULL) {
            if (log)
                log->Report ("ERROR! LZOSTREAM::Deflate: Not enough memory for compression (at least %ld more bytes needed)..\n", LZO1X_999_MEM_COMPRESS);

            free (pOut);
            return false;
        }

        ret = lzo1x_999_compress (pointer, size, pOut, &outSize, wrkmem);
        break;
    }

    free (wrkmem);
    wrkmem = NULL;

    if (ret != LZO_E_OK) {
        if (log)
            log->Report ("ERROR! LZOSTREAM::Deflate: Compression failed with error %d\n", ret);

        free (pOut);
        return -1;
    }

    return outSize;
}
